using LightCAD.MathLib;
using System;
using System.Collections;
using System.Collections.Generic;
using static LightCAD.MathLib.Constants;

namespace LightCAD.Three
{
    public class AnimationMixer : EventDispatcher
    {
        public class StatsItem
        {
            public Func<int> totalAction;
            public Func<int> inuseAction;
            public int total => totalAction();
            public int inUse => inuseAction();
        }
        public class Stats
        {
            private AnimationMixer scope;
            public StatsItem actions = new StatsItem();
            public StatsItem bindings = new StatsItem();
            public StatsItem controlInterpolants = new StatsItem();

            public Stats(AnimationMixer scope)
            {
                this.scope = scope;
                this.actions.totalAction = () => scope._actions.Length;
                this.actions.inuseAction = () => scope._nActiveActions;
                this.bindings.totalAction = () => scope._bindings.Length;
                this.bindings.inuseAction = () => scope._nActiveBindings;
                this.controlInterpolants.totalAction = () => scope._controlInterpolants.Length;
                this.controlInterpolants.inuseAction = () => scope._nActiveControlInterpolants;
            }
        }
        public class ActionsForClip
        {
            public ListEx<AnimationAction> knownActions;
            public JsObj<AnimationAction> actionByRoot;
        }
        #region scope properties or methods
        //private static double[] _controlInterpolantsResultBuffer = new double[1];
        private static AnimationMixerContext getContext()
        {
            return ThreeThreadContext.GetCurrThreadContext().AnimationMixerCtx;
        }
        #endregion

        #region Properties



        public IAnimationObject _root;
        public int _accuIndex;

        public double time;
        public double timeScale;

        private ListEx<AnimationAction> _actions;
        private int _nActiveActions;
        private JsObj<ActionsForClip> _actionsByClip;
        private ListEx<PropertyMixer> _bindings;
        private int _nActiveBindings;
        private ListEx<Interpolant> _controlInterpolants;
        private int _nActiveControlInterpolants;
        private Stats stats;
        private JsObj<JsObj<PropertyMixer>> _bindingsByRootAndName;

        #endregion

        #region constructor
        public AnimationMixer(IAnimationObject root) : base()
        {
            this._root = root;
            this._initMemoryManager();
            this._accuIndex = 0;
            this.time = 0;
            this.timeScale = 1.0;
        }
        #endregion

        #region methods
        public void _bindAction(AnimationAction action, AnimationAction prototypeAction)
        {
            var root = action._localRoot ?? this._root;
            var tracks = action._clip.tracks;
            var nTracks = tracks.Length;
            var bindings = action._propertyBindings;
            var interpolants = action._interpolants;
            var rootUuid = root.uuid;
            var bindingsByRoot = this._bindingsByRootAndName;
            var bindingsByName = bindingsByRoot[rootUuid];
            if (bindingsByName == null)
            {
                bindingsByName = new JsObj<PropertyMixer>();
                bindingsByRoot[rootUuid] = bindingsByName;
            }
            for (int i = 0; i != nTracks; ++i)
            {
                var track = tracks[i];
                var trackName = track.name;
                var binding = bindingsByName[trackName];
                if (binding != null)
                {
                    ++binding.referenceCount;
                    bindings[i] = binding;
                }
                else
                {
                    binding = bindings[i];
                    if (binding != null)
                    {
                        // existing binding, make sure the cache knows
                        if (binding._cacheIndex == null)
                        {
                            ++binding.referenceCount;
                            this._addInactiveBinding(binding, rootUuid, trackName);
                        }
                        continue;
                    }
                    var path = prototypeAction != null ? (prototypeAction._propertyBindings[i].binding as PropertyBinding).parsedPath : null;
                    binding = new PropertyMixer(
                        PropertyBinding.create(root, trackName, path),
                        track.ValueTypeName, track.getValueSize());

                    ++binding.referenceCount;
                    this._addInactiveBinding(binding, rootUuid, trackName);
                    bindings[i] = binding;
                }
                interpolants[i].ResultBuffer = binding.buffer as double[];
            }
        }
        public void _activateAction(AnimationAction action)
        {
            if (!this._isActiveAction(action))
            {
                if (action._cacheIndex == null)
                {
                    // this action has been forgotten by the cache, but the user
                    // appears to be still using it -> rebind
                    var rootUuid = (action._localRoot ?? this._root).uuid;
                    var clipUuid = action._clip.uuid;
                    var actionsForClip = this._actionsByClip[clipUuid];
                    this._bindAction(action, actionsForClip != null ? actionsForClip.knownActions[0] : null);
                    this._addInactiveAction(action, clipUuid, rootUuid);
                }
                var bindings = action._propertyBindings;
                // increment reference counts / sort out state
                for (int i = 0, n = bindings.Length; i != n; ++i)
                {
                    var binding = bindings[i];
                    if (binding.useCount++ == 0)
                    {
                        this._lendBinding(binding);
                        binding.saveOriginalState();
                    }
                }
                this._lendAction(action);
            }
        }
        public void _deactivateAction(AnimationAction action)
        {
            if (this._isActiveAction(action))
            {
                var bindings = action._propertyBindings;
                // decrement reference counts / sort out state
                for (int i = 0, n = bindings.Length; i != n; ++i)
                {
                    var binding = bindings[i];
                    if (--binding.useCount == 0)
                    {
                        binding.restoreOriginalState();
                        this._takeBackBinding(binding);
                    }
                }
                this._takeBackAction(action);
            }
        }
        public void _initMemoryManager()
        {
            this._actions = new ListEx<AnimationAction>(); // "nActiveActions" followed by inactive ones
            this._nActiveActions = 0;
            this._actionsByClip = new JsObj<ActionsForClip>();
            // inside:
            // {
            // 	knownActions: Array< AnimationAction > - used as prototypes
            // 	actionByRoot: AnimationAction - lookup
            // }
            this._bindings = new ListEx<PropertyMixer>(); // "nActiveBindings" followed by inactive ones
            this._nActiveBindings = 0;
            this._bindingsByRootAndName = new JsObj<JsObj<PropertyMixer>>();// inside: Map< name, PropertyMixer >
            this._controlInterpolants = new ListEx<Interpolant>(); // same game as above
            this._nActiveControlInterpolants = 0;
            var scope = this;
            this.stats = new Stats(scope);
        }
        public bool _isActiveAction(AnimationAction action)
        {
            var index = action._cacheIndex;
            return index != null && index < this._nActiveActions;
        }
        public void _addInactiveAction(AnimationAction action, string clipUuid, string rootUuid)
        {
            var actions = this._actions;
            var actionsByClip = this._actionsByClip;
            var actionsForClip = actionsByClip[clipUuid];
            if (actionsForClip == null)
            {
                actionsForClip = new ActionsForClip
                {
                    knownActions = new ListEx<AnimationAction> { action },
                    actionByRoot = new JsObj<AnimationAction>()

                };
                action._byClipCacheIndex = 0;
                actionsByClip[clipUuid] = actionsForClip;
            }
            else
            {
                var knownActions = actionsForClip.knownActions;
                action._byClipCacheIndex = knownActions.Length;
                knownActions.Push(action);
            }
            action._cacheIndex = actions.Length;
            actions.Push(action);
            actionsForClip.actionByRoot[rootUuid] = action;
        }
        public void _removeInactiveAction(AnimationAction action)
        {
            var actions = this._actions;
            var lastInactiveAction = actions[actions.Length - 1];
            var cacheIndex = action._cacheIndex;
            lastInactiveAction._cacheIndex = cacheIndex;
            actions[cacheIndex.Value] = lastInactiveAction;
            actions.Pop();
            action._cacheIndex = null;
            var clipUuid = action._clip.uuid;
            var actionsByClip = this._actionsByClip;
            var actionsForClip = actionsByClip[clipUuid];
            var knownActionsForClip = actionsForClip.knownActions;
            var lastKnownAction = knownActionsForClip[knownActionsForClip.Length - 1];
            var byClipCacheIndex = action._byClipCacheIndex;
            lastKnownAction._byClipCacheIndex = byClipCacheIndex;
            knownActionsForClip[byClipCacheIndex.Value] = lastKnownAction;
            knownActionsForClip.Pop();
            action._byClipCacheIndex = null;
            var actionByRoot = actionsForClip.actionByRoot;
            var rootUuid = (action._localRoot ?? this._root).uuid;
            actionByRoot[rootUuid] = null;
            if (knownActionsForClip.Length == 0)
            {
                actionsByClip[clipUuid] = null;
            }
            this._removeInactiveBindingsForAction(action);
        }
        public void _removeInactiveBindingsForAction(AnimationAction action)
        {
            var bindings = action._propertyBindings;
            for (int i = 0, n = bindings.Length; i != n; ++i)
            {
                var binding = bindings[i];
                if (--binding.referenceCount == 0)
                {
                    this._removeInactiveBinding(binding);
                }
            }
        }
        public void _lendAction(AnimationAction action)
        {
            // [ active actions |  inactive actions  ]
            // [  active actions >| inactive actions ]
            //                 s        a
            //                  <-swap->
            //                 a        s
            var actions = this._actions;
            var prevIndex = action._cacheIndex;
            var lastActiveIndex = this._nActiveActions++;
            var firstInactiveAction = actions[lastActiveIndex];
            action._cacheIndex = lastActiveIndex;
            actions[lastActiveIndex] = action;
            firstInactiveAction._cacheIndex = prevIndex;
            actions[prevIndex.Value] = firstInactiveAction;
        }
        public void _takeBackAction(AnimationAction action)
        {
            // [  active actions  | inactive actions ]
            // [ active actions |< inactive actions  ]
            //        a        s
            //         <-swap->
            //        s        a
            var actions = this._actions;
            var prevIndex = action._cacheIndex;
            var firstInactiveIndex = --this._nActiveActions;
            var lastActiveAction = actions[firstInactiveIndex];
            action._cacheIndex = firstInactiveIndex;
            actions[firstInactiveIndex] = action;
            lastActiveAction._cacheIndex = prevIndex;
            actions[prevIndex.Value] = lastActiveAction;
        }
        public void _addInactiveBinding(PropertyMixer binding, string rootUuid, string trackName)
        {
            var bindingsByRoot = this._bindingsByRootAndName;
            var bindings = this._bindings;
            var bindingByName = bindingsByRoot[rootUuid];
            if (bindingByName == null)
            {
                bindingByName = new JsObj<PropertyMixer>();
                bindingsByRoot[rootUuid] = bindingByName;
            }
            bindingByName[trackName] = binding;
            binding._cacheIndex = bindings.Length;
            bindings.Push(binding);
        }
        public void _removeInactiveBinding(PropertyMixer binding)
        {
            var bindings = this._bindings;
            var propBinding = binding.binding as PropertyBinding;
            var rootUuid = propBinding.rootNode.uuid;
            var trackName = propBinding.path;
            var bindingsByRoot = this._bindingsByRootAndName;
            var bindingByName = bindingsByRoot[rootUuid];
            var lastInactiveBinding = bindings[bindings.Length - 1];
            var cacheIndex = binding._cacheIndex;
            lastInactiveBinding._cacheIndex = cacheIndex;
            bindings[cacheIndex.Value] = lastInactiveBinding;
            bindings.Pop();
            bindingByName[trackName] = null;
            if (bindingByName.Count == 0)
            {
                bindingsByRoot[rootUuid] = null;
            }
        }
        public void _lendBinding(PropertyMixer binding)
        {
            var bindings = this._bindings;
            var prevIndex = binding._cacheIndex.Value;
            var lastActiveIndex = this._nActiveBindings++;
            var firstInactiveBinding = bindings[lastActiveIndex];
            binding._cacheIndex = lastActiveIndex;
            bindings[lastActiveIndex] = binding;
            firstInactiveBinding._cacheIndex = prevIndex;
            bindings[prevIndex] = firstInactiveBinding;
        }
        public void _takeBackBinding(PropertyMixer binding)
        {
            var bindings = this._bindings;
            var prevIndex = binding._cacheIndex.Value;
            var firstInactiveIndex = --this._nActiveBindings;
            var lastActiveBinding = bindings[firstInactiveIndex];
            binding._cacheIndex = firstInactiveIndex;
            bindings[firstInactiveIndex] = binding;
            lastActiveBinding._cacheIndex = prevIndex;
            bindings[prevIndex] = lastActiveBinding;
        }
        public Interpolant _lendControlInterpolant()
        {
            var interpolants = this._controlInterpolants;
            var lastActiveIndex = this._nActiveControlInterpolants++;
            var interpolant = interpolants[lastActiveIndex];
            if (interpolant == null)
            {
                var _controlInterpolantsResultBuffer = getContext()._controlInterpolantsResultBuffer;
                interpolant = new LinearInterpolant(
                    new double[2], new double[2],
                    1, _controlInterpolantsResultBuffer);
                interpolant.__cacheIndex = lastActiveIndex;
                interpolants[lastActiveIndex] = interpolant;
            }
            return interpolant;
        }
        public void _takeBackControlInterpolant(Interpolant interpolant)
        {
            var interpolants = this._controlInterpolants;
            var prevIndex = interpolant.__cacheIndex;
            var firstInactiveIndex = --this._nActiveControlInterpolants;
            var lastActiveInterpolant = interpolants[firstInactiveIndex];
            interpolant.__cacheIndex = firstInactiveIndex;
            interpolants[firstInactiveIndex] = interpolant;
            lastActiveInterpolant.__cacheIndex = prevIndex;
            interpolants[prevIndex] = lastActiveInterpolant;
        }
        public AnimationAction clipAction(object clip, Object3D optionalRoot = null, int? blendMode = null)
        {
            var root = optionalRoot ?? this._root;
            var rootUuid = root.uuid;
            AnimationClip clipObject = clip is string ? AnimationClip.findByName(root, (string)clip) : (AnimationClip)clip;
            var clipUuid = clipObject != null ? clipObject.uuid : (string)clip;
            var actionsForClip = this._actionsByClip[clipUuid];
            AnimationAction prototypeAction = null;
            if (blendMode == null)
            {
                if (clipObject != null)
                {
                    blendMode = clipObject.blendMode;
                }
                else
                {
                    blendMode = NormalAnimationBlendMode;
                }
            }
            if (actionsForClip != null)
            {
                var existingAction = actionsForClip.actionByRoot[rootUuid];
                if (existingAction != null && existingAction.blendMode == blendMode)
                {
                    return existingAction;
                }
                // we know the clip, so we don"t have to parse all
                // the bindings again but can just copy
                prototypeAction = actionsForClip.knownActions[0];
                // also, take the clip from the prototype action
                if (clipObject == null)
                    clipObject = prototypeAction._clip;
            }
            // clip must be known when specified via string
            if (clipObject == null) return null;
            // allocate all resources required to run it
            var newAction = new AnimationAction(this, clipObject, optionalRoot, blendMode.Value);
            this._bindAction(newAction, prototypeAction);
            // and make the action known to the memory manager
            this._addInactiveAction(newAction, clipUuid, rootUuid);
            return newAction;
        }
        public AnimationAction existingAction(object clip, Object3D optionalRoot = null)
        {
            var root = optionalRoot ?? this._root;
            var rootUuid = root.uuid;
            var clipObject = clip is string ? AnimationClip.findByName(root, (string)clip) : (AnimationClip)clip;
            var clipUuid = clipObject != null ? clipObject.uuid : (string)clip;
            var actionsForClip = this._actionsByClip[clipUuid];
            if (actionsForClip != null)
            {
                return actionsForClip.actionByRoot[rootUuid];
            }
            return null;
        }
        public AnimationMixer stopAllAction()
        {
            var actions = this._actions;
            var nActions = this._nActiveActions;
            for (int i = nActions - 1; i >= 0; --i)
            {
                actions[i].stop();
            }
            return this;
        }
        public AnimationMixer update(double deltaTime)
        {
            deltaTime *= this.timeScale;
            var actions = this._actions;
            var nActions = this._nActiveActions;
            var time = this.time += deltaTime;
            var timeDirection = Math.Sign(deltaTime);
            var accuIndex = this._accuIndex ^= 1;
            // run active actions
            for (int i = 0; i != nActions; ++i)
            {
                var action = actions[i];
                action._update(time, deltaTime, timeDirection, accuIndex);
            }
            // update scene graph
            var bindings = this._bindings;
            var nBindings = this._nActiveBindings;
            for (int i = 0; i != nBindings; ++i)
            {
                bindings[i].apply(accuIndex);
            }
            return this;
        }
        public AnimationMixer setTime(double timeInSeconds)
        {
            this.time = 0; // Zero out time attribute for AnimationMixer object;
            for (int i = 0; i < this._actions.Length; i++)
            {
                this._actions[i].time = 0; // Zero out time attribute for all associated AnimationAction objects.
            }
            return this.update(timeInSeconds); // Update used to set exact time. Returns "this" AnimationMixer object.
        }
        public IAnimationObject getRoot()
        {
            return this._root;
        }
        public void uncacheClip(AnimationClip clip)
        {
            var actions = this._actions;
            var clipUuid = clip.uuid;
            var actionsByClip = this._actionsByClip;
            var actionsForClip = actionsByClip[clipUuid];
            if (actionsForClip != null)
            {
                // note: just calling _removeInactiveAction would mess up the
                // iteration state and also require updating the state we can
                // just throw away
                var actionsToRemove = actionsForClip.knownActions;
                for (int i = 0, n = actionsToRemove.Length; i != n; ++i)
                {
                    var action = actionsToRemove[i];
                    this._deactivateAction(action);
                    var cacheIndex = action._cacheIndex;
                    var lastInactiveAction = actions[actions.Length - 1];
                    action._cacheIndex = null;
                    action._byClipCacheIndex = null;
                    lastInactiveAction._cacheIndex = cacheIndex;
                    actions[cacheIndex.Value] = lastInactiveAction;
                    actions.Pop();
                    this._removeInactiveBindingsForAction(action);
                }
                actionsByClip[clipUuid] = null;
            }
        }
        public void uncacheRoot(Object3D root)
        {
            var rootUuid = root.uuid;
            var actionsByClip = this._actionsByClip;
            foreach (var item in actionsByClip)
            {
                var clipUuid = item.Key;
                var actionByRoot = actionsByClip[clipUuid].actionByRoot;
                var action = actionByRoot[rootUuid];
                if (action != null)
                {
                    this._deactivateAction(action);
                    this._removeInactiveAction(action);
                }
            }
            var bindingsByRoot = this._bindingsByRootAndName;
            var bindingByName = bindingsByRoot[rootUuid];
            if (bindingByName != null)
            {
                foreach (var item in bindingByName)
                {
                    var trackName = item.Key;
                    var binding = bindingByName[trackName];
                    binding.restoreOriginalState();
                    this._removeInactiveBinding(binding);
                }
            }
        }
        public void uncacheAction(AnimationClip clip, Object3D optionalRoot = null)
        {
            var action = this.existingAction(clip, optionalRoot);
            if (action != null)
            {
                this._deactivateAction(action);
                this._removeInactiveAction(action);
            }
        }
        #endregion

    }
}
